//returns group object with passed id
//if there is no group with this id, new one is created
sCanvas.fn.group = function(id) {
	var obj = this.get(id);

	if (obj.isNull()) {
		if (sCanvas.isValidId(id)) {
			obj = new CanvasGroup(this, id);
			this.addIdObject(obj, id);
		} else {
			throw new Error("Passed ID is not valid.");
		}
	} else if (!sCanvas.isGroup(obj)) {
		throw new Error("Object identified by passed ID is not a group.");
	}

	return obj;
};



var 
	//parent of all canvas objects
	CanvasGroup = function(sc, id) {
		this.sc = sc;
		this.objects = [ ];
		this.id = id;
		return this;
	};

CanvasGroup.prototype = {

	//stores sCanvas reference
	sc: null,

	//stores count of grouped objects
	length: 0,

	//stores references to grouped objects
	objects: null,

	//stores group id
	id: null,

	//adds object to the group
	add: function(obj) {
		//only non-null and non-group object is added and only if it is not already in group
		if (!obj.isNull() && !sCanvas.isGroup(obj) && !this.has(obj)) {
			this.objects.push(obj);
			this.length = this.objects.length;
			obj.group(this.id);
		}
		return this;
	},

	//if called with no argument or false, group is destroyed, but objects live on
	//if called with true, group and all grouped objects are destroyed
	//if called with object, this object is removed from group
	//if object is wanted to be removed, call remove on this object directly
	//if called with integer, object at this position is removed
	//if object at position is wanted to be removed, call group.get(i).remove()
	remove: function(obj) {
		if (obj === undefined) {
			obj = false;
		}

		var i;

		if (typeof obj === "boolean") {
			if (obj) {
				for (i = this.objects.length - 1; i >= 0; i--) {
					this.objects[i].remove();
				}
			} else {
				for (i = this.objects.length - 1; i >= 0; i--) {
					this.objects[i].ungroup(this.id);
				}
			}
			this.sc.removeIdObject(this.id);

			return this.sc.nullObject();
		} else if (typeof obj === "number") {
			i = Math.floor(obj);
		} else {
			i = this.objects.indexOf(obj);
		}

		if (i >= 0 && i < this.objects.length) {
			this.objects[i].ungroup(this.id);
			this.objects.splice(i, 1);
		}

		this.length = this.objects.length;

		return this;
	},

	//returns true if group contains passed object
	has: function(obj) {
		return this.objects.indexOf(obj) !== -1;
	},

	//returns object at i position
	get: function(i) {
		if (this.objects[i] !== undefined) {
			return this.objects[i];
		} else {
			return this.sc.nullObject();
		}
	},

	//calls passed function on all grouped objects
	each: function(fn) {
		var i, ret;

		if (typeof fn !== "function") {
			throw new TypeError("Function was not passed.");
		}

		for (i in this.objects) {
			ret = fn.apply(this.objects[i], [ Number(i) ]);
			if (ret === false) {
				break;
			}
		}
		
		return this;
	},

	//returns true if group is empty
	isEmpty: function() {
		return this.length === 0;
	},

	//returns false as group is never considered null
	isNull: function() {
		return false;
	},

	//makes object visible
	show: function() {
		if (this.isEmpty()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].show();
		}

		return this;
	},

	//makes object invisible
	hide: function() {
		if (this.isEmpty()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].hide();
		}

		return this;
	},

//	//returns true if all objects are visible
//	isVisible: function() {
//		var i, result = true;
//
//		if (this.length === 0) {
//			result = false;
//		} else {
//			for (i in this.objects) {
//				if (!this.objects[i].isVisible()) {
//					result = false;
//					break;
//				}
//			}
//		}
//
//		return result;
//	},

	//returns true if first object is visible
	isVisible: function() {
		if (this.isEmpty()) {
			return false;
		}
		
		return this.get(0).isVisible();
	},

//	//returns object containing info about the most top, right, bottom and left point of object
//	box: function() {
//		throw new Error("Method CanvasObject.box() should be overriden!");
//	},
	
	//gets/sets over object
	over: function(obj) {
		return this;
		//TODO: think of calling over() of most top grouped object
	},

	//gets/sets under object
	under: function(obj) {
		return this;
		//TODO: think of calling under() of most bottom grouped object
	},

	//returns attribute value of first grouped object or
	//sets attribute(s) value of all grouped objects
	attr: function(name, value) {
		if (this.isEmpty()) {
			return this;
		}

		var i, result = this;

		if (typeof name === "string" && value === undefined) {
			//getting value
			result = this.get(0).attr(name);
		} else {
			//setting value(s)
			try {
				for (i in this.objects) {
					this.objects[i].attr(name, value);
				}
			} catch (e) {}
		}

		return result;
	},

	//registers animation to all grouped objects
	animate: function(attr, duration) {
		if (this.isEmpty()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].animate(attr, duration);
		}
		return this;
	},
	
	//adds function to animation queue of all grouped objects
	queue: function(fn) {
		if (this.isEmpty()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].queue(fn);
		}
		return this;
	},

	//delay execution of animations in animation queue of all grouped objects
	delay: function(duration) {
		if (this.isEmpty()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].delay(duration);
		}
		return this;
	},

	//stop currently running animation of all grouped objects
	stop: function(clear, jump) {
		if (this.isEmpty()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].stop(clear, jump);
		}
		return this;
	},

	//removes all animations of all grouped objects that have not started
	clearQueue: function() {
		if (this.isEmpty()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].clearQueue();
		}
		return this;
	},

	//immediatelly finishes whole animation queue of all grouped objects
	finish: function() {
		if (this.isEmpty()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].finish();
		}
		return this;
	},

	//pauses running animation of all grouped objects
	pause: function() {
		if (this.isEmpty()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].pause();
		}
		return this;
	},

	//reruns paused animation of all grouped objects
	play: function() {
		if (this.isEmpty()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].play();
		}
		return this;
	},

	//returns true if first object animation is paused
	isPaused: function() {
		if (this.isEmpty()) {
			return false;
		}

		return this.get(0).isPaused();
	},

	//binds event handler to all grouped objects
	bind: function(event, fn) {
		if (this.isEmpty()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].bind(event, fn);
		}
		return this;
	},

	//unbinds event handler(s) of all grouped objects
	unbind: function(event, fn) {
		if (this.isEmpty()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].unbind(event, fn);
		}
		return this;
	},

	//triggers event of all grouped objects
	trigger: function(event, data) {
		if (this.isEmpty()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].trigger(event, data);
		}
		return this;
	},

	//checks if specified point is in first object
	isPointIn: function(x, y) {
		if (this.isEmpty()) {
			return false;
		}

		return this.get(0).isPointIn(x, y);
	},
	
	//returns data of first grouped object or
	//sets data to all grouped objects
	data: function(key, value) {
		if (this.isEmpty()) {
			return this;
		}
		
		var i;
		
		if (arguments.length === 0) {
			//getting all associated data
			return this.get(0).data();
		} else if (arguments.length === 1) {
			//getting value
			return this.get(0).data(key);
		} else {
			//setting value
			for (i in this.objects) {
				this.objects[i].data(key, value);
			}
			
			return this;
		}
	},
	
	//removes data associated with all grouped objects
	removeData: function(key) {
		if (this.isEmpty()) {
			return this;
		}
		
		var i;
		for (i in this.objects) {
			this.objects[i].removeData(key);
		}
		return this;
	},
	
	//returns true if first object has associated data with passed key
	//if no key is passed, returns true if any data has been associated with first object
	hasData: function(key) {
		if (this.isEmpty()) {
			return false;
		}
		
		return this.get(0).hasData(key);
	}

};

//add event shortcuts
var gevents = (
	"click dblclick mouseup mousedown"
	+ " mouseenter mousemove mouseleave"
).split(" ");

for (var gei = 0; gei < gevents.length; gei++) {
	CanvasGroup.prototype[gevents[gei]] = (function() {
		var type = gevents[gei];
		return function(fn) {
			return fn === undefined ? this.trigger(type) : this.bind(type, fn);
		};
	}());
}

CanvasGroup.prototype.hover = function(fnIn, fnOut) {
	return this.bind("mouseenter", fnIn).bind("mouseleave", fnOut || fnIn);
};